Référence ********* Lvalue et rvalue ================= Cette section introduit des concepts particulièrement difficiles à maîtriser du langage C++. Leur présentation, en apparence accessible, ne saurait résumer toute leur complexité. De ce fait, nous n'allons pas donner leurs définitions exactes mais nous allons essayer de les présenter de manière pragmatique. Toute information d'un programme est stockée en mémoire. Cependant, certaines entités ont des durées de vie courtes qui se limitent à leurs évaluations. Par exemple, lorsque l'on écrit *(a+b)+c*, nous allons tout d'abord évaluer le résultat de *a+b*. Ce résultat existe et il est stocké quelque-part en mémoire, nommons le *r*. L'étape suivante consiste à prendre ce résultat intermédiaire *r* et à calculer *r+c*. Une fois l'opération effectuée, *r* devient obsolète et cet élément est détruit. La mémoire qu'il occupait est à nouveau disponible. Ainsi le résultat *r* n'existe plus en mémoire, il ne peut plus être consulté, d'ailleurs, il n'a jamais eu officiellement de nom. .. panels:: :column: col-lg-10 p-2 Définition: **Lvalue et rvalue** | Une expression dont la durée de vie est limitée à son évaluation représente une **rvalue**. | Une expression dont la durée de vie peut dépasser l'évaluation de cette expression est appelée une **lvalue**. Prenons un exemple : .. code-block:: cpp A = B + 1 L'écriture *A* correspond à la variable *A* dont la durée de vie perdure au delà de cette ligne de code. Cette variable à gauche du signe = correspond donc à une lvalue. L'expression *B+1* désigne un résultat temporaire dont la durée de vie est éphémère, c'est une rvalue. .. note:: On ne doit pas limiter la notion de lvalue aux variables. En effet, la définition des lvalues s'étend aux expressions, ce qui est plus large que l'écriture *A=*. Prenons un exemple : l'écriture *T[i]=* est une expression (le i-ème élément de T). Mais, c'est aussi une lvalue car sa durée de vie en mémoire dépasse l'évaluation de cette expression. .. warning:: Historiquement, une lvalue désigne une expression pouvant se trouver à gauche (left) d'une affectation. Mais, une lvalue peut tout aussi bien se trouver à droite comme par exemple dans l'écriture *A = A*. Une rvalue désigne une expression que l'on trouve à droite (right) d'une affectation. Mais une r-value ne peut jamais se trouver à gauche. Par exemple, l'écriture *B+1 = A* lève une erreur. .. note:: Rvalue et lvalue permettent de classer les expressions en deux groupes. Ne vous trompez pas, ce ne sont pas des types. Nous n'allons pas aller plus dans les détails car cela deviendrait très difficile. Cependant, nous tenions à parler de la notion de lvalue et de rvalue car les messages d'erreur du compilateur y font souvent allusion. Une astuce pour vérifier si une expression peut être une lvalue consiste à écrire cette expression à gauche d'une affectation pour voir si cela a un sens : * a + b = 5; // a+b correspond à un résultat temporaire, impossible * abs(-a) = 5; // abs(-a) désigne un résultat temporaire, impossible .. quiz:: rvaluelvalue :title: Lvalue et rvalue. Indiquez si chaque affirmation est vraie ou fausse : .. csv-table:: :widths: 10 :delim: ! :quiz:`{"type":"TF","answer":"F"}` Lvalue et rvalue correspondent à des types :quiz:`{"type":"TF","answer":"T"}` Une lvalue peut se situer à droite et à gauche dans une affectation :quiz:`{"type":"TF","answer":"T"}` Une rvalue peut se situer uniquement à droite dans une affectation :quiz:`{"type":"TF","answer":"F"}` Une lvalue se définit par le caractère éphémère du résultat qu'elle représente :quiz:`{"type":"TF","answer":"T"}` Une lvalue permet de stocker un résultat :quiz:`{"type":"TF","answer":"T"}` Si *T* est un tableau, l'expression *T[0]* désigne une lvalue :quiz:`{"type":"TF","answer":"F"}` L'expression *a+b* désigne une lvalue Les références ============== Référence de variables ---------------------- Pour créer une référence vers une variable existante, il suffit d'utiliser la syntaxe suivante : .. panels:: :column: col-lg-10 p-2 **SYNTAXE** - Création d'une référence Type & NomRef = lvalue; Une **référence** définit un alias que l'on peut utiliser à la place d'une expression lvalue. Ce scénario inclut aussi le cas d'un alias vers une variable. Aucune copie n'est effectuée, la référence et la donnée originale désignent une même chose. Utiliser l'une ou l'autre est équivalent. Nous pouvons définir des références sur des variables mais aussi sur des lvalues plus complexes. Voici un exemple : .. code-block:: cpp #include int main() { int a = 7; int & b = a; // définition d'une référence vers la variable a std::cout << b; // ==>> 7 int T[4] = { 1, 2, 3, 4}; int & c = T[2]; // définition d'une référence vers T[2] std::cout << c; // ==>> 3 } La syntaxe nous interdit de créer une référence sans l'initialiser. Ainsi, on ne peut écrire : .. code-block:: cpp int main() { int & a; ==> syntaxe IMPOSSIBLE - il faut une initialisation } Vérification ------------ Veuillez exécuter le code suivant puis analyser les résultats et confirmer que la référence n'effectue pas de copie : .. code-block:: cpp #include struct v { int x; int y; }; int main() { // type entier int a = 10; int & b = a; b++; std::cout << a << " " << b << std::endl; // type struct v B; B.x = 10; B.y = 20; v &A = B; A.x += 5; A.y += 5; std::cout << A.x << " " << A.y << " --- " << B.x << " " << B.y << std::endl; } Référence et passage d'arguments -------------------------------- Un paramètre de fonction qui correspond à une référence ne définit pas une nouvelle variable mais un alias renommant la donnée passée en argument. Par conséquent, toutes les modifications effectuées sur la référence sont en fait effectuées sur la donnée passée en argument. Voici la syntaxe : .. panels:: :column: col-lg-10 p-2 **SYNTAXE** - Passage d'un argument par référence : ... NomFonction(TypeVar & NomRef, ...) { ... } .. note:: L'utilisation de références dans les paramètres de fonction permet de retourner plusieurs informations. L'instruction *return* ne permet elle que de retourner un unique élément ce qui peut parfois être limité. Veuillez exécuter le code suivant puis analyser les résultats et confirmer qu'aucune copie n'est effectuée : .. code-block:: cpp #include struct v { int x; int y; }; void F(int & b, v & B) { b += 1; B.x += 2; B.y += 3; } int main() { // type entier int a = 10; // type struct v A; A.x = 10; A.y = 20; std::cout << "Avant l'appel : " << a << " / " << A.x << " " << A.y << std::endl; F(a,A); std::cout << "Après l'appel : " << a << " / " << A.x << " " << A.y << std::endl; } Exercices --------- .. panels:: Fonctions disponibles ^^^^^^^^^^^^^^^^^^^^^ .. code-block:: cpp int F(int t) { t += 2; return t; } void I(int & t) { t += 7; } int Z(int & t) { t = 1; return 5; } int F(int & a, int & b) { return a+b; } --- Exercice 1 .. code-block:: cpp int main() { int a; Z(a); I(a); std::cout << a; } --- Exercice 2 .. code-block:: cpp int main() { int a = 8; a += F(a); I(a); std::cout << a; } --- Exercice 3 .. code-block:: cpp int main() { int a = 8; std::cout << I(a+8); } --- Exercice 4 .. code-block:: cpp int main() { int a = 8; int b = 3; std::cout << F(a,b); } --- Exercice 5 .. code-block:: cpp int main() { int a = 8; int b = 3; std::cout << F(F(b,a)); } .. quiz:: RefTest :title: Premiers pas avec les références. Pour chaque exemple, indiquez l'affichage obtenu ou ERR si le programme émet une erreur : .. csv-table:: :delim: ! Exercice 1 :quiz:`{"type":"FB","answer":"8"}` ! Exercice 4 :quiz:`{"type":"FB","answer":"11"}` Exercice 2 :quiz:`{"type":"FB","answer":"25"}` ! Exercice 5 :quiz:`{"type":"FB","answer":"13"}` Exercice 3 :quiz:`{"type":"FB","answer":"ERR"}` Exercices --------- .. panels:: Fonctions disponibles ^^^^^^^^^^^^^^^^^^^^^ .. code-block:: cpp int F(int & t) { t += 2; return t; } void I(int & t) { t += 7; } int & Z(int & t) { t = 0; return t; } int & F(int & a, int & b) { return a; } --- Exercice 6 .. code-block:: cpp int main() { int a = 8; int b = 3; std::cout << F(F(b,a)); } --- Exercice 7 .. code-block:: cpp int main() { int a = 8; std::cout << Z(a+1); } --- Exercice 8 .. code-block:: cpp int main() { int a = 8; int b = 3; Z(F(a,b))++; std::cout << a; } --- Exercice 9 .. code-block:: cpp int main() { int a = 8; std::cout << F(F(a)); } --- Exercice 10 .. code-block:: cpp int main() { int a = 3; int b = 2; F(Z(a),Z(b))++; std::cout << a; } .. quiz:: RefTest :title: Exercices sur les références - Niveau 2. Pour chaque exemple, indiquez l'affichage obtenu ou ERR si le programme émet une erreur : .. csv-table:: :delim: ! Exercice 6 :quiz:`{"type":"FB","answer":"5"}` ! Exercice 9 :quiz:`{"type":"FB","answer":"ERR"}` Exercice 7 :quiz:`{"type":"FB","answer":"ERR"}` ! Exercice 10 :quiz:`{"type":"FB","answer":"1"}` Exercice 8 :quiz:`{"type":"FB","answer":"1"}` Les cas difficiles ================== Retour de référence depuis une variable locale ---------------------------------------------- Techniquement, vous pouvez définir une fonction retournant une référence : .. code-block:: cpp int & Test(int a) { int b = a * a + 3; return b; } int main() { int a = 7; int & c = Test(a); c++; // cette ligne déclenche une erreur système std::cout << c; } La syntaxe de ce programme est correcte. Le code compile, pourtant son exécution produit une erreur système inconnue. Que se passe-t-il ? * Dans la fonction *Test*, nous créons une variable *b*. Cela est autorisée. * Nous retournons ensuite une référence vers cette variable *b*. * Cette référence sert à initialiser la référence *c* maintenant associée à *b*. * L'exécution de la ligne : *c++;* pourtant correcte, produit une erreur système. Remontons à la source du problème. La référence *c* est faite vers la variable locale *b* de la fonction *Test()*. Hors cette variable est une variable locale dont la durée de vie s'arrête à la fin de la fonction. Ainsi, cette variable est détruite et la référence retournée désigne une variable n'existant plus. Lorsque l'on essaye d'utiliser cette référence, c'est le crash. A noter que le comportement du programme est indéterminé dans ce genre de situation : * Le programme peut crasher avec ou sans message (souvent sans). * Une autre variable peut être modifiée à la place de *b*, produisant un bug incompréhensible plus tard dans le programme. * Une partie de la mémoire contenant du code peut être modifiée ce qui fait que le programme peut se bloquer. .. panels:: :column: col-lg-10 p-2 **REGLE** : On ne doit pas retourner une référence sur une variable locale à une fonction. Une syntaxe d'appel ambiguë --------------------------- Nous présentons dans le code ci-dessous, un passage par copie et un passage par référence : .. code-block:: cpp #include void F1(int a) { a+= 1; std::cout << a << std::endl; } void F2(int &a) { a+= 1; std::cout << a << std::endl; } int main() { int a = 5; F1(a); // passage par copie F2(a); // passage par référence } Le code ne produit pas d'erreur, il semble fonctionner mais là n'est pas le problème. Examinons la définition des deux fonctions disponibles. L'une utilise un passage par copie et l'autre par référence. A ce niveau, tout est clairement explicité grâce à l’utilisation du signe &. Examinons maintenant les appels dans la fonction *main()*, nous avons : *F1(a)* et *F2(a)*. A ce niveau, on ne constate aucune différence de syntaxe. .. panels:: :column: col-lg-10 p-2 **ATTENTION** : En examinant un appel de fonction, il est impossible de déterminer si les variables sont passées par copie ou par référence. C'est une faiblesse du langage. Référence en lecture seule -------------------------- Le qualificatif **const** sur une référence est utilisé pour que le compilateur interdise toute modification de l'élément référencé. Utiliser une *const* référence sert : * de documentation pour le programmeur. * à protéger un argument de toute manipulation non désirée. Voici un exemple : .. code-block:: cpp #include void F1(int &a) { std::cout << a; // autorisée, il s'agit juste d'une lecture a++; // autorisée, la référence est modifiée } void F2(const int &a) { std::cout << a; // autorisée, il s'agit juste d'une lecture a++; // ERREUR de compilation, tentative de modification d'une const référence } Lorsque vous lisez une déclaration de la forme : .. code-block:: cpp void F(const T & obj); vous savez que vous pouvez transmettre votre élément de type *T* sans risque qu'il soit modifié par cette fonction. Perte de performance -------------------- Lorsque l'on transmet un élément prenant de la place mémoire, il est préférable de le passer par référence afin d'éviter une recopie des données. Cependant, si vous effectuez un passage par copie, vous n'aurez pas de message d'erreur, mais vous pourrez constater un ralentissement du programme. Exercices --------- .. quiz:: RefTest222 :title: Les cas complexes Pour chaque exemple, indiquez l'affirmation est vraie ou fausse : .. csv-table:: :delim: ! :quiz:`{"type":"TF","answer":"F"}` Si une fonction retourne une référence sur une variable locale, cela la maintient en vie. :quiz:`{"type":"TF","answer":"F"}` L'écriture *F(a)* correspond uniquement à un passage par copie. :quiz:`{"type":"TF","answer":"T"}` Une const référence indique que l'on ne peut modifier l'élément référencé. :quiz:`{"type":"TF","answer":"T"}` Le passage par référence permet de limiter le temps passé à faire des copies.